////////////////////////////////////////////////////////////////////////////////
// Includes
////////////////////////////////////////////////////////////////////////////////

// To log all computation in a log file
#define LOG_COMPUTATION

// To force SC computation to stop when recycling the last item
#define BATCH_MODE
#include "InterruptionException.hpp"

// Header
#include "AISOPlugin.hpp"

// Macros
#include "aiso_macros.h"

// String manipulation
#include <string>

// Conversion functions
#include "Convert.hpp"

// Tool macros
#include "TKMacros.h"

// Vector
#include <vector>
// Pair
#include <utility>

// Math
#include <cmath>

// Streams
#include <iostream>
#include <fstream>

// Namespaces
using namespace std;
using namespace Toolkit;
using namespace SC;

// Union to get float as an unsigned int
typedef union floatuint {
	float f;
	unsigned int i;
};

////////////////////////////////////////////////////////////////////////////////
// Experiment data collection
////////////////////////////////////////////////////////////////////////////////

#ifdef LOG_COMPUTATION
	static ofstream log_stream;
	static unsigned int iter_cpt;
#endif

////////////////////////////////////////////////////////////////////////////////
// Recycling system memory: Stored here rather than in the systems for memory
// and speed reasons (i.e., works as if it was part of the kernel/schema)
////////////////////////////////////////////////////////////////////////////////

// Classes id and distribution probability
static vector< pair<unsigned int, double> > classes_info;
// Classes data
static vector< vector< vector<float> > > classes_data;

////////////////////////////////////////////////////////////////////////////////
// Plugin init and exit
////////////////////////////////////////////////////////////////////////////////

void InitPlugin (const string & prog_name) {
#ifdef LOG_COMPUTATION
	iter_cpt = 0;
#endif
	// Read problem data
	string file_name = "aiso.in";
	ifstream in_data(file_name.c_str());
	if (!in_data.is_open()) {
		cerr << "Cannot open " << file_name << endl;
		return;
	}
	// Number of classes
	unsigned int nb_classes;
	in_data >> nb_classes;
	// Read classes
	double tot_dist_factor = 0.;
	for (unsigned int i=0; i<nb_classes; ++i) {
		// Class id
		unsigned int class_id;
		in_data >> class_id;
		// Number of samples
		unsigned int nb_samples;
		in_data >> nb_samples;
		// Sample size
		unsigned int sample_size;
		in_data >> sample_size;
		// Class distribution factor
		double dist_factor;
		in_data >> dist_factor;
		// Values
		vector< vector<float> > class_data;
		vector<float> data;
		data.resize(sample_size);
		for (unsigned int j=0; j<nb_samples; ++j) {
			for (unsigned int k=0; k<sample_size; ++k) {
				in_data >> data[k];
				if (data[k] == 9999.f)
					data[k] = std::numeric_limits<float>::quiet_NaN();
			}
			class_data.push_back(data);
		}
		classes_data.push_back(class_data);
		classes_info.push_back( pair<unsigned int, double>(class_id, dist_factor));
		tot_dist_factor += dist_factor;
	}
	for (unsigned int i=0; i<classes_info.size(); ++i)
		classes_info[i].second /= tot_dist_factor;
	// Close file
	in_data.close();
#ifdef LOG_COMPUTATION
	// Open log stream
	file_name = prog_name + ".log";
	log_stream.open(file_name.c_str());
	if (!log_stream.is_open()) {
		cerr << "Cannot open " << file_name << endl;
		return;
	}
#endif
}

void ClosePlugin () {
#ifdef LOG_COMPUTATION
	// Close log stream
	if (log_stream.is_open()) log_stream.close();
#endif
}

////////////////////////////////////////////////////////////////////////////////
// SC Callbacks
////////////////////////////////////////////////////////////////////////////////

#define WASTE_STATE			"000"
#define FOOD_STATE			"001"
#define CELL_STATE 			"010"
#define ADHESION_STATE		"111"
#define DANGER_SIGNAL_STATE	"100"

bool RecycleWaste (SCSystem & universe, const SCSystem & recycling, SCSystem & waste) {
	if (classes_info.empty()) return false;
	// Pick up a class (roulette-wheel)
	double	r = frand(),
			cpt = classes_info[0].second;
	int idx = 0;
	while ((cpt < r) && (idx < (int)classes_info.size()-1)) {
		++idx;
		cpt += classes_info[idx].second;
	}
	// Pick up a sample in this class
	vector<float> & sample =  classes_data[idx][rand()%classes_data[idx].size()];
	if (sample.empty()) return false;
	// Set sample in data system
	waste.ReplaceSchema1(UIntToBStr(classes_info[idx].first, DAT_S1_CLASS_LGH), DAT_S1_CLASS_OFF, DAT_S1_CLASS_LGH);
	waste.ReplaceSchema1(UIntToBStr(sample.size(), DAT_S1_NB_DATA_LGH), DAT_S1_NB_DATA_OFF, DAT_S1_NB_DATA_LGH);
	unsigned int data_size = sizeof(floatuint)*8;
	waste.ReplaceSchema1(UIntToBStr(data_size, DAT_S1_DATA_SIZE_LGH), DAT_S1_DATA_SIZE_OFF, DAT_S1_DATA_SIZE_LGH);
	floatuint val;
	for (unsigned int i=0; i<sample.size(); ++i) {
		val.f = sample[i];
		waste.ReplaceSchema2(UIntToBStr(val.i, data_size), DAT_S2_DATA_OFF + i*data_size, data_size);
	}
	// Give data/food state to the data/waste system
	waste.ReplaceKernel(FOOD_STATE, DAT_K_FOOD_OFF, DAT_K_FOOD_LGH);
	// Increase the given samples counter in the universe
	unsigned int nb_samples = BStrToUInt(universe.GetSchema1(UNI_S1_CPT_SAMPLES_OFF, UNI_S1_CPT_SAMPLES_LGH));
	++nb_samples;
	universe.ReplaceSchema1(UIntToBStr(nb_samples, UNI_S1_CPT_SAMPLES_LGH), UNI_S1_CPT_SAMPLES_OFF, UNI_S1_CPT_SAMPLES_LGH);
	if (nb_samples >= BStrToUInt(universe.GetSchema2(UNI_S2_MAX_SAMPLES_OFF, UNI_S2_MAX_SAMPLES_LGH))) {
		universe.ReplaceKernel(UIntToBStr(0, UNI_K_MORE_SAMPLES_LGH), UNI_K_MORE_SAMPLES_OFF, UNI_K_MORE_SAMPLES_LGH);
#ifdef BATCH_MODE
		throw Exceptions::InterruptionException("Experiment finished");
#endif
	}
	// Computation occurred
#ifdef LOG_COMPUTATION
	++iter_cpt;
#endif
	return true;
}

bool Absorb (SCSystem & food, const SCSystem & absorption, SCSystem & organism) {
	// Transform data/food into data/cell
	food.ReplaceKernel(CELL_STATE, DAT_K_CELL_OFF, DAT_K_CELL_LGH);
	// Set cell age to zero
	food.ReplaceKernel(UIntToBStr(0, DAT_K_AGE_LGH), DAT_K_AGE_OFF, DAT_K_AGE_LGH);
	// Set cell alive
	food.ReplaceKernel(UIntToBStr(1, DAT_K_ALIVE_LGH) , DAT_K_ALIVE_OFF, DAT_K_ALIVE_LGH);
	// Set max obtained links to zero
	food.ReplaceKernel(UIntToBStr(0, DAT_K_MAX_LINKS_LGH) , DAT_K_MAX_LINKS_OFF, DAT_K_MAX_LINKS_LGH);
	// Decrease organism's hunger
	unsigned int hunger = BStrToUInt(organism.GetSchema1(ORG_S1_HUNGER_OFF, ORG_S1_HUNGER_LGH));
	--hunger;
	organism.ReplaceSchema1( UIntToBStr(hunger, ORG_S1_HUNGER_LGH), ORG_S1_HUNGER_OFF, ORG_S1_HUNGER_LGH);
	if (hunger == 0)
		organism.ReplaceKernel("0", ORG_K_IS_HUNGRY_OFF, ORG_K_IS_HUNGRY_LGH);
	// (Optional info: Increase organism size)
	organism.ReplaceSchema2( UIntToBStr(BStrToUInt(organism.GetSchema2(ORG_S2_SIZE_OFF, ORG_S2_SIZE_LGH))+1, ORG_S2_SIZE_LGH), ORG_S2_SIZE_OFF, ORG_S2_SIZE_LGH);
	// Organism takes the Food/Cell from the common super systems
	organism.TakeFromSuperSystems(food, absorption);
#ifdef LOG_COMPUTATION
	log_stream << iter_cpt << " ABS " << reinterpret_cast<unsigned int>(&food) << " " << BStrToUInt(food.GetSchema1(DAT_S1_CLASS_OFF, DAT_S1_CLASS_LGH)) << endl;
	++iter_cpt;
#endif
	// Computation occurred
	return true;
}

bool Grow (SCSystem & cell, const SCSystem & growth, SCSystem & adhesion) {
	// Amount and size of data in cells and adhesion
	unsigned int c_nb_data = BStrToUInt(cell.GetSchema1(DAT_S1_NB_DATA_OFF, DAT_S1_NB_DATA_LGH)),
				 c_data_size = BStrToUInt(cell.GetSchema1(DAT_S1_DATA_SIZE_OFF, DAT_S1_DATA_SIZE_LGH));
	// Get cell data vector
	vector<float> vc(c_nb_data);
	floatuint val;
	for (unsigned int i=0; i<c_nb_data; ++i) {
		val.i = BStrToUInt(cell.GetSchema2(DAT_S2_DATA_OFF + i*c_data_size, c_data_size));
		vc[i] = val.f;
	}
	// Get cell connections counter
	unsigned int c_cpt = BStrToUInt(cell.GetKernel(DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH));
	// Get adhesion connections counter
	unsigned int a_cpt = BStrToUInt(adhesion.GetKernel(DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH));
	// If adhesion vector has not the same size, set up adhesion data from cell's
	if ((c_cpt == 0) && (a_cpt == 0)) {
		// If the cell can grab the adhesion, set adhesion data
		if (cell.Grab(adhesion)) {
			adhesion.ReplaceSchema1(UIntToBStr(c_nb_data, DAT_S1_NB_DATA_LGH), DAT_S1_NB_DATA_OFF, DAT_S1_NB_DATA_LGH);
			adhesion.ReplaceSchema1(UIntToBStr(c_data_size, DAT_S1_DATA_SIZE_LGH), DAT_S1_DATA_SIZE_OFF, DAT_S1_DATA_SIZE_LGH);
			for (unsigned int i=0; i<c_nb_data; ++i)
				adhesion.ReplaceSchema2(cell.GetSchema2(DAT_S2_DATA_OFF + i*c_data_size, c_data_size), DAT_S2_DATA_OFF + i*c_data_size, c_data_size);
			// Set adhesion connections counter to one
			adhesion.ReplaceKernel( UIntToBStr(1, DAT_K_NB_LINKS_LGH), DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH);
			// Set cell connections counter to one
			cell.ReplaceKernel( UIntToBStr(1, DAT_K_NB_LINKS_LGH), DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH);
#ifdef LOG_COMPUTATION
			log_stream << iter_cpt << " GRW " << reinterpret_cast<unsigned int>(&adhesion) << " " << reinterpret_cast<unsigned int>(&cell) << endl;
			++iter_cpt;
#endif
			// Some computation occurred
			return true;
		}
	}
	// Otherwise if the distance between data is below threshold, link and average data
	else {
		// Get adhesion data vector and compute the distance
		vector<float> va(c_nb_data);
		float dist = 0.f;
		for (unsigned int i=0; i<c_nb_data; ++i) {
			val.i = BStrToUInt(adhesion.GetSchema2(DAT_S2_DATA_OFF + i*c_data_size, c_data_size));
			va[i] = val.f;
			if (!isnan(vc[i]) && !isnan(va[i]))
				dist += pow(vc[i]-va[i],2.f);
		}
		dist = sqrt(dist);
		// If the distance is below the threshold
		float threshold = (static_cast<float>(BStrToUInt(growth.GetKernel(GRW_K_SIM_THRESHOLD_OFF, GRW_K_SIM_THRESHOLD_LGH))))/100.f;
		if (dist <= threshold) {
			// If the cell can grab the adhesion, average adhesion data
			if (cell.Grab(adhesion)) {
				// Update adhesion data vector
				for (unsigned int i=0; i<c_nb_data; ++i) {
					if (!isnan(vc[i])) {
						if (!isnan(va[i]))
							va[i] = (va[i] * ((float)a_cpt) + vc[i])/((float)(a_cpt+1));
						else
							va[i] = vc[i];
					}
					val.f = va[i];
					adhesion.ReplaceSchema2(UIntToBStr(val.i, c_data_size), DAT_S2_DATA_OFF + i*c_data_size, c_data_size);
				}
				// Increase the adhesion connections counter
				adhesion.ReplaceKernel(UIntToBStr(a_cpt+1, DAT_K_NB_LINKS_LGH), DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH);
				// Increase the cell connections counter
				cell.ReplaceKernel(UIntToBStr(c_cpt+1, DAT_K_NB_LINKS_LGH), DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH);
#ifdef LOG_COMPUTATION
				log_stream << iter_cpt << " GRW " << reinterpret_cast<unsigned int>(&adhesion) << " " << reinterpret_cast<unsigned int>(&cell) << endl;
				++iter_cpt;
#endif
				// Some computation occurred
				return true;
			}
		}
	}
	// No computation occurred
	return false;
}

bool Decay (SCSystem & data, const SCSystem & decay, SCSystem & time) {
	// Get age
	unsigned int c_age = BStrToUInt(data.GetKernel(DAT_K_AGE_OFF, DAT_K_AGE_LGH)),
	// Get TTL
				 c_ttl = BStrToUInt(decay.GetKernel(DEC_K_DATA_TTL_OFF, DEC_K_DATA_TTL_LGH));
	// Increase the age if under ttl
	if (c_age < c_ttl)
		data.ReplaceKernel(UIntToBStr(c_age+1, DAT_K_AGE_LGH) , DAT_K_AGE_OFF, DAT_K_AGE_LGH);
	else {
		data.ReplaceKernel(UIntToBStr(0, DAT_K_DEAD_LGH) , DAT_K_DEAD_OFF, DAT_K_DEAD_LGH);
#ifdef LOG_COMPUTATION
		log_stream << iter_cpt << " DTH " << reinterpret_cast<unsigned int>(&data) << endl;
#endif
	}
	// Computation occurred
#ifdef LOG_COMPUTATION
	++iter_cpt;
#endif
	return true;
}

bool Split (SCSystem & cell, const SCSystem & split, SCSystem & adhesion) {
	if (cell.Release(adhesion)) {
		// Get the adhesion connections counter
		unsigned int a_cpt = BStrToUInt(adhesion.GetKernel(DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH));
		// Amount and size of data in cells and adhesion
		unsigned int a_nb_data = BStrToUInt(adhesion.GetSchema1(DAT_S1_NB_DATA_OFF, DAT_S1_NB_DATA_LGH)),
					 a_data_size = BStrToUInt(adhesion.GetSchema1(DAT_S1_DATA_SIZE_OFF, DAT_S1_DATA_SIZE_LGH));
		// Update adhesion data vector
		string str;
		floatuint val_c, val_a;
		for (unsigned int i=0; i<a_nb_data; ++i) {
			val_c.i = BStrToUInt(cell.GetSchema2(DAT_S2_DATA_OFF + i*a_data_size, a_data_size));
			val_a.i = BStrToUInt(adhesion.GetSchema2(DAT_S2_DATA_OFF + i*a_data_size, a_data_size));
			if (!isnan(val_c.f))
				if (!isnan(val_a.f))
					val_a.f = (val_a.f * ((float)a_cpt) - val_c.f)/((float)(a_cpt-1));
			adhesion.ReplaceSchema2(UIntToBStr(val_a.i, a_data_size), DAT_S2_DATA_OFF + i*a_data_size, a_data_size);
		}
		// Decrease the adhesion connection counter
		--a_cpt;
		adhesion.ReplaceKernel( UIntToBStr(a_cpt, DAT_K_NB_LINKS_LGH), DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH);
		// Decrease the cell connection counter
		cell.ReplaceKernel( UIntToBStr(BStrToUInt(cell.GetKernel(DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH))-1, DAT_K_NB_LINKS_LGH), DAT_K_NB_LINKS_OFF, DAT_K_NB_LINKS_LGH);
		// Eventually update the max connections of the cell to other cells
		unsigned int c_max_links = BStrToUInt(cell.GetKernel(DAT_K_MAX_LINKS_OFF, DAT_K_MAX_LINKS_LGH));
		if (c_max_links < a_cpt)
			cell.ReplaceKernel( UIntToBStr(a_cpt, DAT_K_MAX_LINKS_LGH), DAT_K_MAX_LINKS_OFF, DAT_K_MAX_LINKS_LGH);
#ifdef LOG_COMPUTATION
		log_stream << iter_cpt << " SPL " << reinterpret_cast<unsigned int>(&adhesion) << " " << reinterpret_cast<unsigned int>(&cell) << endl;
		++iter_cpt;
#endif
		// Computation occurred
		return true;
	}
	// Should not happen
	else {
		cerr << "ERROR: Cell could not release adhesion" << endl;
		return false;
	}
}

bool RecycleCell (SCSystem & cell, const SCSystem & recycler, SCSystem & organism) {
	// Set age to zero
	cell.ReplaceKernel(UIntToBStr(0, DAT_K_AGE_LGH), DAT_K_AGE_OFF, DAT_K_AGE_LGH);
	// Set alive
	cell.ReplaceKernel(UIntToBStr(1, DAT_K_ALIVE_LGH) , DAT_K_ALIVE_OFF, DAT_K_ALIVE_LGH);
	// Transform the cell into an adhesion surface if the cell was part of a tissue
	if (BStrToUInt(cell.GetKernel(DAT_K_MAX_LINKS_OFF, DAT_K_MAX_LINKS_LGH)) > 0) {
		cell.ReplaceKernel(ADHESION_STATE, DAT_K_ADHESION_OFF, DAT_K_ADHESION_LGH);
#ifdef LOG_COMPUTATION
		log_stream << iter_cpt << " ADH " << reinterpret_cast<unsigned int>(&cell) << " ";
		log_stream << BStrToUInt(cell.GetSchema1(DAT_S1_CLASS_OFF, DAT_S1_CLASS_LGH)) << endl;
#endif
	}
	// Transform the cell into a danger signal otherwise
	else {
		cell.ReplaceKernel(DANGER_SIGNAL_STATE, DAT_K_DANGER_SIGNAL_OFF, DAT_K_DANGER_SIGNAL_LGH);
#ifdef LOG_COMPUTATION
		log_stream << iter_cpt << " DGR " << reinterpret_cast<unsigned int>(&cell) << " ";
		log_stream << BStrToUInt(cell.GetSchema1(DAT_S1_CLASS_OFF, DAT_S1_CLASS_LGH)) << endl;
#endif
	}
	// Increase organism's hunger
	unsigned int hunger = BStrToUInt(organism.GetSchema1(ORG_S1_HUNGER_OFF, ORG_S1_HUNGER_LGH));
	++hunger;
	organism.ReplaceSchema1( UIntToBStr(hunger, ORG_S1_HUNGER_LGH), ORG_S1_HUNGER_OFF, ORG_S1_HUNGER_LGH);
	if (hunger > 0)
		organism.ReplaceKernel("1", ORG_K_IS_HUNGRY_OFF, ORG_K_IS_HUNGRY_LGH);
	// Computation occurred
#ifdef LOG_COMPUTATION
	++iter_cpt;
#endif
	return true;
}

bool Expel (SCSystem & data, const SCSystem & expelling, SCSystem & organism) {
	// Transform a data dead body into data/waste
	data.ReplaceKernel(WASTE_STATE, DAT_K_WASTE_OFF, DAT_K_WASTE_LGH);
	// Expel
	organism.ReleaseToSuperSystems(data);
	// (Optional: Decrease organism's size)
	unsigned int org_size = BStrToUInt(organism.GetSchema2(ORG_S2_SIZE_OFF, ORG_S2_SIZE_LGH));
	if (org_size > 0)
		organism.ReplaceSchema2(UIntToBStr(org_size-1, ORG_S2_SIZE_LGH), ORG_S2_SIZE_OFF, ORG_S2_SIZE_LGH);
	else cerr << "Organism size was probably not initialised correctly" << endl;
#ifdef LOG_COMPUTATION
	log_stream << iter_cpt << " EXP " << reinterpret_cast<unsigned int>(&data) << endl;
	++iter_cpt;
#endif
	// Computation occurred
	return true;
}

////////////////////////////////////////////////////////////////////////////////
